package org.mx.utils.sm;

import java.math.BigInteger;
import java.security.SecureRandom;

import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECFieldElement.Fp;
import org.mx.utils.TypeUtils;
import org.bouncycastle.math.ec.ECPoint;

import lombok.Getter;

/**
 * SM2为非对称加密，基于ECC。该算法已公开。由于该算法基于ECC，故其签名速度与秘钥生成速度都快于RSA。ECC 256位（SM2采用的就是ECC
 * 256位的一种）安全强度比RSA 2048位高，但运算速度快于RSA。
 * 
 * @author john.peng
 *
 */
public class SM2 {
	/**
	 * 正式参数
	 */
	public static String[] ecc_param = { "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",
			"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC",
			"28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",
			"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123",
			"32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7",
			"BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0" };
	private final BigInteger ecc_p;
	private final BigInteger ecc_a;
	private final BigInteger ecc_b;
	private final BigInteger ecc_n;
	private final BigInteger ecc_gx;
	private final BigInteger ecc_gy;
	private final ECCurve ecc_curve;
	private final ECPoint ecc_point_g;
	private final ECDomainParameters ecc_bc_spec;
	private final ECKeyPairGenerator ecc_key_pair_generator;
	private final ECFieldElement ecc_gx_fieldelement;
	private final ECFieldElement ecc_gy_fieldelement;

	/**
	 * 默认的构造函数
	 */
	public SM2() {
		this.ecc_p = new BigInteger(ecc_param[0], 16);
		this.ecc_a = new BigInteger(ecc_param[1], 16);
		this.ecc_b = new BigInteger(ecc_param[2], 16);
		this.ecc_n = new BigInteger(ecc_param[3], 16);
		this.ecc_gx = new BigInteger(ecc_param[4], 16);
		this.ecc_gy = new BigInteger(ecc_param[5], 16);
		this.ecc_gx_fieldelement = new Fp(this.ecc_p, this.ecc_gx);
		this.ecc_gy_fieldelement = new Fp(this.ecc_p, this.ecc_gy);

		this.ecc_curve = new ECCurve.Fp(this.ecc_p, this.ecc_a, this.ecc_b);
		this.ecc_point_g = new ECPoint.Fp(this.ecc_curve, this.ecc_gx_fieldelement, this.ecc_gy_fieldelement);
		this.ecc_bc_spec = new ECDomainParameters(this.ecc_curve, this.ecc_point_g, this.ecc_n);

		ECKeyGenerationParameters ecc_ecgenparam;
		ecc_ecgenparam = new ECKeyGenerationParameters(this.ecc_bc_spec, new SecureRandom());
		this.ecc_key_pair_generator = new ECKeyPairGenerator();
		this.ecc_key_pair_generator.init(ecc_ecgenparam);
	}

	/**
	 * 加密方法
	 * 
	 * @param publicKey 公钥
	 * @param data      待加密数据
	 * @return 加密后的数据
	 */
	public byte[] encrypt(byte[] publicKey, byte[] data) {
		if (publicKey == null || publicKey.length == 0) {
			return null;
		}
		if (data == null || data.length == 0) {
			return null;
		}
		byte[] source = new byte[data.length];
		System.arraycopy(data, 0, source, 0, data.length);
		Cipher cipher = new Cipher();
		ECPoint userKey = ecc_curve.decodePoint(publicKey);

		ECPoint c1 = cipher.initEncrypt(this, userKey);
		cipher.encrypt(source);
		byte[] c3 = new byte[32];
		cipher.dofinal(c3);

		byte[] c1b = c1.getEncoded();
		byte[] c2b = source;
		byte[] c3b = c3;
		byte[] out = new byte[c1b.length + c2b.length + c3b.length];
		System.arraycopy(c1b, 0, out, 0, c1b.length);
		System.arraycopy(c2b, 0, out, c1b.length, c2b.length);
		System.arraycopy(c3b, 0, out, c1b.length + c2b.length, c3b.length);
		return out;
	}

	/**
	 * 解密方法
	 * 
	 * @param privateKey    私钥
	 * @param encryptedData 待解密数据
	 * @return 解密后的数据
	 */
	public byte[] decrypt(byte[] privateKey, byte[] encryptedData) {
		if (privateKey == null || privateKey.length == 0) {
			return null;
		}
		if (encryptedData == null || encryptedData.length == 0) {
			return null;
		}
		// 加密字节数组转换为十六进制的字符串 长度变为encryptedData.length * 2
		String data = TypeUtils.byteArray2HexString(encryptedData);
		/***
		 * 分解加密字串 （C1 = C1标志位2位 + C1实体部分128位 = 130） （C3 = C3实体部分64位 = 64） （C2 =
		 * encryptedData.length * 2 - C1长度 - C2长度）
		 */
		byte[] c1Bytes = TypeUtils.hexString2ByteArray(data.substring(0, 130));
		int c2Len = encryptedData.length - 97;
		byte[] c2 = TypeUtils.hexString2ByteArray(data.substring(130, 130 + 2 * c2Len));
		byte[] c3 = TypeUtils.hexString2ByteArray(data.substring(130 + 2 * c2Len, 194 + 2 * c2Len));
		BigInteger userD = new BigInteger(1, privateKey);

		// 通过C1实体字节来生成ECPoint
		ECPoint c1 = ecc_curve.decodePoint(c1Bytes);
		Cipher cipher = new Cipher();
		cipher.initDecrypt(userD, c1);
		cipher.decrypt(c2);
		cipher.dofinal(c3);
		// 返回解密结果
		return c2;
	}

	/**
	 * 创建密钥对
	 * 
	 * @return 密钥对
	 */
	public static SM2KeyPair generateKeyPair() {
		SM2 sm2 = new SM2();
		AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
		ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
		ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
		return new SM2KeyPair(ecpriv.getD(), ecpub.getQ());
	}

	/**
	 * SM2密钥对定义
	 * 
	 * @author john.peng
	 *
	 */
	@Getter
	public static class SM2KeyPair {
		private BigInteger privateKey;
		private ECPoint publicKey;

		/**
		 * 默认的构造函数
		 * 
		 * @param privateKey 私钥
		 * @param publicKey  公钥
		 */
		public SM2KeyPair(BigInteger privateKey, ECPoint publicKey) {
			super();
			this.privateKey = privateKey;
			this.publicKey = publicKey;
		}
	}

	/**
	 * Cipher类
	 * 
	 * @author john.peng
	 *
	 */
	private class Cipher {
		private int ct;
		private ECPoint p2;
		private SM3Digest sm3keybase;
		private SM3Digest sm3c3;
		private byte key[];
		private byte keyOff;

		/**
		 * 默认的构造函数
		 */
		public Cipher() {
			super();
			this.ct = 1;
			this.key = new byte[32];
			this.keyOff = 0;
		}

		/**
		 * reset
		 */
		private void reset() {
			this.sm3keybase = new SM3Digest();
			this.sm3c3 = new SM3Digest();
			byte p[] = TypeUtils.bigint2ByteArray(p2.getX().toBigInteger());
			this.sm3keybase.update(p, 0, p.length);
			this.sm3c3.update(p, 0, p.length);
			p = TypeUtils.bigint2ByteArray(p2.getY().toBigInteger());
			this.sm3keybase.update(p, 0, p.length);
			this.ct = 1;
			nextKey();
		}

		/**
		 * nextKey
		 */
		private void nextKey() {
			SM3Digest sm3keycur = new SM3Digest(this.sm3keybase);
			sm3keycur.update((byte) (ct >> 24 & 0xff));
			sm3keycur.update((byte) (ct >> 16 & 0xff));
			sm3keycur.update((byte) (ct >> 8 & 0xff));
			sm3keycur.update((byte) (ct & 0xff));
			sm3keycur.doFinal(key, 0);
			this.keyOff = 0;
			this.ct++;
		}

		/**
		 * 初始化加密过程
		 * 
		 * @param sm2     SM2对象
		 * @param userKey 密钥
		 * @return ECPoint
		 */
		public ECPoint initEncrypt(SM2 sm2, ECPoint userKey) {
			AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
			ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
			ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
			BigInteger k = ecpriv.getD();
			ECPoint c1 = ecpub.getQ();
			this.p2 = userKey.multiply(k);
			reset();
			return c1;
		}

		/**
		 * 加密
		 * 
		 * @param data 数据
		 */
		public void encrypt(byte data[]) {
			this.sm3c3.update(data, 0, data.length);
			for (int i = 0; i < data.length; i++) {
				if (keyOff == key.length) {
					nextKey();
				}
				data[i] ^= key[keyOff++];
			}
		}

		/**
		 * 初始化解密过程
		 * 
		 * @param userD 密钥
		 * @param c1    c1
		 */
		public void initDecrypt(BigInteger userD, ECPoint c1) {
			this.p2 = c1.multiply(userD);
			reset();
		}

		/**
		 * 解密
		 * 
		 * @param data 数据
		 */
		public void decrypt(byte data[]) {
			for (int i = 0; i < data.length; i++) {
				if (keyOff == key.length) {
					nextKey();
				}
				data[i] ^= key[keyOff++];
			}

			this.sm3c3.update(data, 0, data.length);
		}

		/**
		 * dofinal
		 * 
		 * @param c3 c3
		 */
		public void dofinal(byte c3[]) {
			byte p[] = TypeUtils.bigint2ByteArray(p2.getY().toBigInteger());
			this.sm3c3.update(p, 0, p.length);
			this.sm3c3.doFinal(c3, 0);
			reset();
		}
	}
}
